Is there a way to set an interrupt to go off after a certain period of time?
The easiest way to achieve your purpose seems to be to configure a
timer to fire every 33&nbsp;ms, and use this interrupt to wake up the
MCU. For this, you first have to choose what timer to use. On the Mega, I
would rather use the 16-bit timer, but the ATtiny85 only has 8-bit
timers. Next you have to choose a prescaler value (clock frequency
division) among those available for that timer. The prescaler should be
large enough to avoid overflowing your timer, but smaller prescaler
values give better time resolution.

Then you configure the timer to run in “CTC mode” (clear timer on
compare match). In this mode, it counts from zero to the value that you
programmed in the OCRxA register (where x is the number identifying
the timer), and then goes back to zero. This way you can get an
interrupt every OCRxA + 1 timer cycles.

In the example code below, you have to replace x with the appropriate
timer number, and check the datasheet for the bits to set in the control
registers TCCR*:



#include &lt;avr/interrupt.h&gt;

// Interrupt period, in prescaled clock cycles.
const uint16_t PERIOD = ...;  // or uint8_t for an 8-bit timer.

// The timer interrupt is used ONLY to wake the CPU, so there is no job
// to be done inside the interrupt service routine.
EMPTY_INTERRUPT(TIMERx_COMPA_vect);

void setup()
{
    // Disable Timer 0 interrupts.
    // Warning: this will prevent the Arduino from keeping time.
    TIMSK0 = 0;

    // Configure Timer x.
    TCCRxA = 0;            // undo the timer config done...
    TCCRxB = 0;            // ...by the Arduino core library
    TCNTx = 0;             // reset the timer
    OCRxA = PERIOD - 1;    // set the period
    TIMSKx = _BV(OCIExA);  // enable TIMERx_COMPA interrupt
    TCCRxA = ...;          // set the counting mode to CTC...
    TCCRxB = ...;          // ...and set the prescaler
}

void loop()
{
    // Sleep while waiting for the timer interrupt.
    sleep_mode();

    // Now that we have been waken up...
    do_the_job();
}


Edit: As noticed by Gerben, the Arduino core library configures
Timer&nbsp;0 to send periodic interrupts (one every 1024&nbsp;µs), which
are used for timekeeping (millis(), delay() and co.). This
interrupts will wake up the MCU and prevent it from sleeping as long as
you want. The simplest solution, which is implemented in the code above,
is to disable the Timer&nbsp;0 interrupts. The drawback is that the
Arduino timekeeping functions will not work anymore.

If you need those timekeeping functions, then the proper solution is the
one sketched by Gerben in his comment: set a boolean in the interrupt
that can be checked in the main loop. Here is the naive implementation
of this idea: EMPTY_INTERRUPT(TIMERx_COMPA_vect); is replaced by

volatile bool time_elapsed;

ISR(TIMERx_COMPA_vect)
{
    time_elapsed = true;
}


Then, in the main loop you sleep repeatedly until the ISR sets the flag:

void loop()
{
    // Warning: race condition here!
    while (!time_elapsed)
        sleep_mode();
    time_elapsed = false;

    // Now that we have been waken up...
    do_the_job();
}


As stated in the comment, this naive implementation suffers from what is
known as a race condition. The problem is in the following scenario:


The main loop reads the value of time_elapsed and it turns out to
be false
At this point the interrupt fires and the ISR sets that boolean to
true
Returning from the ISR, the main loop does not test the flag again
(since it just did it) and goes straight to sleep_mode();.


At this point the MCU ends up sleeping although it should be performing
the job which is due now.

This race condition may not a be a big deal to you: The MCU will be
eventually woken up by the Timer&nbsp;0 interrupt no more than 1024&nbsp;µs
later. In some cases it is an issue, but fortunately there is a proper
solution. It is described in the page about sleeping of the avr-libc
documentation. It relies on the fact that when interrupts are
disabled, then enabled again with sei(), the machine instruction right
after the sei() is guaranteed to be executed before any interrupt is
serviced. Then the proper way to test the flag and sleep is:

void loop()
{
    while (!time_elapsed) {
        cli();
        if (!time_elapsed) {
            sleep_enable();
            sei();          // interrupts enabled
            sleep_cpu();    // this will be executed before any ISR
            sleep_disable();
        }
        sei();
    }
    time_elapsed = false;

    // Now that we have been waken up...
    do_the_job();
}


With this version, if the interrupt fires right after the test
if (!time_elapsed), it will be serviced after sleep_cpu(), which
will have the effect of waking the MCU right away.