Claude-skill-registry google-sheets

Google Sheets automation using Python gspread library - reading, writing, formatting, and Service Account setup

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/google-sheets-awannaphasch2016-agent-kernel-mcp" ~/.claude/skills/majiayu000-claude-skill-registry-google-sheets && rm -rf "$T"
manifest: skills/data/google-sheets-awannaphasch2016-agent-kernel-mcp/SKILL.md
source content

Google Sheets Automation

Focus: Automating Google Sheets with Python using gspread library

Source: Patterns from Meta Ads → Google Sheets automation project (ss-automation)


When to Use This Skill

Use google-sheets when:

  • ✓ Automating data export to Google Sheets
  • ✓ Building dashboards that update from external data
  • ✓ Creating scheduled reports in Sheets format
  • ✓ Processing data from APIs and writing to Sheets
  • ✓ Setting up Service Account authentication

DO NOT use for:

  • ✗ Complex spreadsheet logic (use Google Apps Script for that)
  • ✗ Real-time collaborative editing (Sheets UI is better)
  • ✗ Simple one-time manual edits

Why Python + gspread (Not Apps Script)

Criteriagspread + PythonGoogle Apps Script
External API integration✅ Native (requests, SDKs)⚠️ UrlFetchApp (limited)
Data manipulation✅ pandas, numpy❌ JavaScript arrays only
Version control✅ git, code review❌ Web editor only
Testing✅ pytest, mocks❌ Manual testing
Scheduling✅ cron, Lambda, GitHub Actions✅ Built-in triggers
Execution time✅ Unlimited❌ 6-minute limit
Cost$0-5/month$0

Decision: Use gspread + Python when integrating external data (APIs, databases). Use Apps Script for simple sheet-internal automation.


Quick Start

1. Install Dependencies

pip install gspread google-auth gspread-formatting

2. Create Service Account (One-Time Setup)

See SETUP.md for detailed instructions.

Quick version:

  1. Go to Google Cloud Console
  2. Create project → Enable Google Sheets API
  3. Create Service Account → Download JSON key
  4. Save as
    credentials.json
    (gitignore it!)
  5. Share your Google Sheet with service account email

3. Basic Usage

import gspread
from google.oauth2.service_account import Credentials

# Authenticate
SCOPES = [
    'https://www.googleapis.com/auth/spreadsheets',
    'https://www.googleapis.com/auth/drive'
]
creds = Credentials.from_service_account_file('credentials.json', scopes=SCOPES)
client = gspread.authorize(creds)

# Open sheet and write data
sheet = client.open("My Dashboard").sheet1
sheet.update('A1', [['Header1', 'Header2', 'Header3']])
sheet.append_row(['Value1', 'Value2', 'Value3'])

Core Patterns

Pattern 1: Type System Integration

Google Sheets API expects specific types. Always convert Python types properly.

from decimal import Decimal
from datetime import date, datetime

def prepare_row_for_sheets(row: dict) -> list:
    """Convert Python types to Sheets-compatible values."""
    values = []
    for key, value in row.items():
        if isinstance(value, Decimal):
            # Decimal → float (Sheets doesn't understand Decimal)
            values.append(float(value))
        elif isinstance(value, (date, datetime)):
            # Date → ISO string (Sheets can parse this)
            values.append(value.strftime('%Y-%m-%d'))
        elif isinstance(value, bool):
            # Bool → uppercase string (Sheets convention)
            values.append('TRUE' if value else 'FALSE')
        elif value is None:
            # None → empty string (Sheets blank cell)
            values.append('')
        else:
            values.append(value)
    return values

# Usage
row = {
    'date': date(2025, 1, 9),
    'amount': Decimal('19.79'),
    'active': True,
    'notes': None
}
sheet.append_row(prepare_row_for_sheets(row))
# Writes: ['2025-01-09', 19.79, 'TRUE', '']

Pattern 2: Defensive Authentication

Validate credentials exist and work before attempting operations.

from pathlib import Path
import gspread
from google.oauth2.service_account import Credentials

class SheetsClient:
    def __init__(self, credentials_path: str, sheet_name: str):
        # Defensive: Validate credentials file exists
        creds_file = Path(credentials_path)
        if not creds_file.exists():
            raise FileNotFoundError(
                f"Credentials file not found: {credentials_path}\n"
                f"See docs/GOOGLE_SETUP.md for setup instructions."
            )

        self.credentials_path = credentials_path
        self.sheet_name = sheet_name
        self.client = None
        self.sheet = None

    def connect(self):
        """Authenticate and connect to sheet."""
        SCOPES = [
            'https://www.googleapis.com/auth/spreadsheets',
            'https://www.googleapis.com/auth/drive'
        ]

        creds = Credentials.from_service_account_file(
            self.credentials_path,
            scopes=SCOPES
        )
        self.client = gspread.authorize(creds)

        # Defensive: Validate sheet exists and is accessible
        try:
            spreadsheet = self.client.open(self.sheet_name)
            self.sheet = spreadsheet.sheet1
        except gspread.exceptions.SpreadsheetNotFound:
            raise ValueError(
                f"Sheet not found: {self.sheet_name}\n"
                f"Make sure the sheet is shared with the service account email."
            )

        return self

    def write_row(self, row: list):
        """Write a row, with connection validation."""
        if not self.sheet:
            raise RuntimeError("Not connected. Call connect() first.")
        self.sheet.append_row(row)

Pattern 3: Batch Operations

Minimize API calls by batching operations.

def write_multiple_rows(sheet, rows: list[list]):
    """Write multiple rows in single API call."""
    if not rows:
        return

    # Get next empty row
    existing = len(sheet.get_all_values())
    start_row = existing + 1

    # Batch update (single API call vs N calls)
    cell_range = f'A{start_row}'
    sheet.update(cell_range, rows)

# ✅ GOOD: 1 API call for 100 rows
write_multiple_rows(sheet, hundred_rows)

# ❌ BAD: 100 API calls
for row in hundred_rows:
    sheet.append_row(row)  # Each call is an API request!

Pattern 4: Cell Formatting

Apply formatting after writing data.

from gspread_formatting import (
    format_cell_range,
    CellFormat,
    NumberFormat,
    Color,
    TextFormat
)

def setup_sheet_formatting(sheet):
    """Apply standard formatting to sheet."""

    # Header row: Bold, gray background
    header_format = CellFormat(
        backgroundColor=Color(0.9, 0.9, 0.9),
        textFormat=TextFormat(bold=True),
        horizontalAlignment='CENTER'
    )
    format_cell_range(sheet, 'A1:Z1', header_format)

    # Currency columns (e.g., columns F and H)
    currency_format = CellFormat(
        numberFormat=NumberFormat(type='CURRENCY', pattern='$#,##0.00')
    )
    format_cell_range(sheet, 'F:F', currency_format)
    format_cell_range(sheet, 'H:H', currency_format)

    # Percentage columns (e.g., columns G and J)
    percent_format = CellFormat(
        numberFormat=NumberFormat(type='PERCENT', pattern='0.00%')
    )
    format_cell_range(sheet, 'G:G', percent_format)
    format_cell_range(sheet, 'J:J', percent_format)

Pattern 5: API Response Parsing

Parse external API responses with type validation before writing.

from typing import Dict, List, Any
from decimal import Decimal

class APIResponseParser:
    """Parse external API response for Sheets."""

    def __init__(self, response: List[Dict[str, Any]]):
        # Defensive: Validate response structure
        if not response or not isinstance(response, list):
            raise ValueError("Invalid API response format")

        self.raw_data = response

    def parse_row(self, item: Dict[str, Any]) -> Dict[str, Any]:
        """Parse single item with type conversion."""
        # Type conversion: API returns strings, we need proper types
        return {
            'date': item.get('date_start', ''),
            'name': item.get('name', ''),
            # String → int
            'impressions': int(item.get('impressions', '0')),
            'clicks': int(item.get('clicks', '0')),
            # String → Decimal (currency precision)
            'spend': Decimal(item.get('spend', '0')),
            # String → float
            'ctr': float(item.get('ctr', '0')),
        }

    def to_sheets_rows(self) -> List[List[Any]]:
        """Convert all items to Sheets row format."""
        rows = []
        for item in self.raw_data:
            parsed = self.parse_row(item)
            row = [
                parsed['date'],
                parsed['name'],
                parsed['impressions'],
                parsed['clicks'],
                float(parsed['spend']),  # Decimal → float for Sheets
                parsed['ctr'],
            ]
            rows.append(row)
        return rows

    @staticmethod
    def get_headers() -> List[str]:
        """Column headers matching to_sheets_rows() format."""
        return ['Date', 'Name', 'Impressions', 'Clicks', 'Spend', 'CTR']

Common Operations

Read Data

# Get all data
all_values = sheet.get_all_values()

# Get specific range
cell_range = sheet.get('A1:D10')

# Get as dictionaries (first row = headers)
records = sheet.get_all_records()
# Returns: [{'Name': 'Alice', 'Age': 30}, {'Name': 'Bob', 'Age': 25}]

# Find a cell
cell = sheet.find('search term')
print(f"Found at row {cell.row}, col {cell.col}")

Write Data

# Update single cell
sheet.update('A1', 'Hello')

# Update range
sheet.update('A1:C1', [['Col1', 'Col2', 'Col3']])

# Append row (to end of data)
sheet.append_row(['New', 'Row', 'Data'])

# Insert row at specific position
sheet.insert_row(['Inserted', 'Row'], index=2)

# Batch update multiple ranges
sheet.batch_update([
    {'range': 'A1', 'values': [['Header1']]},
    {'range': 'B1', 'values': [['Header2']]},
])

Formulas

# Write formula
sheet.update('D2', '=SUM(A2:C2)')

# Write multiple formulas
formulas = [
    ['=SUM(A2:A10)'],
    ['=AVERAGE(B2:B10)'],
    ['=MAX(C2:C10)'],
]
sheet.update('D2:D4', formulas)

# Summary row with formulas
summary_row = 12
sheet.update(f'A{summary_row}:F{summary_row}', [[
    'TOTAL',
    f'=SUM(B2:B{summary_row-1})',
    f'=SUM(C2:C{summary_row-1})',
    f'=SUM(D2:D{summary_row-1})',
    f'=AVERAGE(E2:E{summary_row-1})',
    f'=SUM(F2:F{summary_row-1})',
]])

Worksheets

# List all worksheets
worksheets = spreadsheet.worksheets()

# Get worksheet by name
ws = spreadsheet.worksheet("Sheet2")

# Create new worksheet
new_ws = spreadsheet.add_worksheet(title="New Sheet", rows=100, cols=20)

# Delete worksheet
spreadsheet.del_worksheet(ws)

# Duplicate worksheet
spreadsheet.duplicate_sheet(source_sheet_id=ws.id, new_sheet_name="Copy")

Error Handling

Common Errors and Solutions

ErrorCauseSolution
SpreadsheetNotFound
Sheet not shared with service accountShare sheet with SA email
APIError: PERMISSION_DENIED
Wrong permissionsGrant "Editor" access
APIError: RATE_LIMIT_EXCEEDED
Too many API callsAdd delays, use batch operations
FileNotFoundError
credentials.json missingDownload from GCP Console
RefreshError
Token expiredRe-download credentials

Retry Pattern

import time
from gspread.exceptions import APIError

def write_with_retry(sheet, data, max_retries=3):
    """Write data with exponential backoff retry."""
    for attempt in range(max_retries):
        try:
            sheet.append_row(data)
            return True
        except APIError as e:
            if 'RATE_LIMIT' in str(e) and attempt < max_retries - 1:
                wait_time = 2 ** attempt  # 1, 2, 4 seconds
                print(f"Rate limited. Waiting {wait_time}s...")
                time.sleep(wait_time)
            else:
                raise
    return False

Scheduling Options

Option 1: Local Cron (Free)

# crontab -e
0 8 * * * cd /path/to/project && python update_sheets.py >> /var/log/sheets.log 2>&1

Option 2: GitHub Actions (Free Tier)

# .github/workflows/daily-sync.yml
name: Daily Sheets Sync

on:
  schedule:
    - cron: '0 8 * * *'  # Daily at 8 AM UTC
  workflow_dispatch:

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - run: pip install -r requirements.txt
      - run: python update_sheets.py
        env:
          GOOGLE_CREDENTIALS: ${{ secrets.GOOGLE_CREDENTIALS }}

Option 3: AWS Lambda

# lambda_handler.py
import json
import base64
import os
from google.oauth2.service_account import Credentials

def handler(event, context):
    # Decode credentials from environment variable
    creds_json = base64.b64decode(os.environ['GOOGLE_CREDENTIALS_B64'])
    creds_dict = json.loads(creds_json)

    creds = Credentials.from_service_account_info(creds_dict)
    client = gspread.authorize(creds)

    # Your logic here
    sheet = client.open("Dashboard").sheet1
    sheet.append_row(['Updated', 'from', 'Lambda'])

    return {'statusCode': 200}

Security Checklist

  • credentials.json
    in
    .gitignore
  • File permissions:
    chmod 600 credentials.json
  • Service Account has minimal permissions (Sheets API only)
  • Sheet shared with specific SA email (not "anyone with link")
  • Rotate keys every 90 days
  • Don't log credentials or access tokens

File Organization

.claude/skills/google-sheets/
├── SKILL.md              # This file (entry point)
├── SETUP.md              # Service Account setup guide
├── PATTERNS.md           # Advanced patterns
└── examples/
    ├── basic_write.py    # Simple read/write example
    └── api_to_sheets.py  # API → Sheets pipeline

Integration with Other Skills

ScenarioUse With
Need .env for credentials pathpython-env
Deploying to Lambdadeployment
Testing Sheets integrationtesting-workflow

References